﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using PostSharp.Laos;
using System.Reflection;
using System.IO;
using System.Diagnostics;
using System.Collections;

namespace EditableBusinessObjects.Postsharp.Public
{
    #region interface to implement
    /// <summary>
    /// the interface that an editable business object implements
    /// </summary>
    public interface IEditableBusinessObject : IEditableObject
    {
        /// <summary>
        /// Stores the value. Called by the field interception aspect
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="newvalue">The newvalue.</param>
        /// <param name="storedvalue">The storedvalue.</param>
        /// <returns>the new value to store</returns>
        object StoreValue(string propertyName, object newvalue, object storedvalue);

        /// <summary>
        /// Gets the value. This will actually just return the storedvalue, but
        /// gives the framework the opportunity to do a deepcopy, which might be
        /// needed. This way, we only do a copy if it is needed!
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="storedvalue">The storedvalue.</param>
        /// <returns>the storedvalue</returns>
        object GetValue(string propertyName, object storedvalue);

        /// <summary>
        /// Gets the original value as available in the original value dictionary
        /// </summary>
        /// <param name="Propertyname">The propertyname.</param>
        /// <returns></returns>
        object GetOriginalValue(string Propertyname);

        /// <summary>
        /// Sets the value as original in the original value dictionary
        /// </summary>
        /// <param name="Propertyname">The propertyname.</param>
        /// <param name="value">The value.</param>
        void SetValueAsOriginal(string Propertyname, object value);

        /// <summary>
        /// Begins the deserialization. We can do setup here
        /// </summary>
        void BeginDeserialization();

        /// <summary>
        /// Ends the deserialization. We can do tear down here
        /// </summary>
        void EndDeserialization();

        /// <summary>
        /// determines if we want to serialize the original value. 
        /// Only wanted if we actually set it
        /// </summary>
        /// <param name="Propertyname">The propertyname.</param>
        /// <returns></returns>
        bool ShouldSerializeOriginalValue(string Propertyname);

    } 

    #endregion

    #region aspect that links interface to object
    /// <summary>
    /// aspect that specifies how the interface is injected into the object
    /// </summary>
    [Serializable]
    public class EditableBusinessObjectAspect : CompositionAspect
    {
        #region state
        /// <summary>
        /// maps properties to fields
        /// </summary>
        Dictionary<string, string> PropertyNameFieldName = new Dictionary<string, string>();
        /// <summary>
        /// maps fields to properties
        /// </summary>
        Dictionary<string, string> FieldNamePropertyName = new Dictionary<string, string>();

        string typename;
        #endregion

        /// <summary>
        /// will create two dictionaries (for performance) that we use at runtime
        /// to match field and property names
        /// </summary>
        /// <param name="type">The type.</param>
        public override void CompileTimeInitialize(Type type)
        {
            base.CompileTimeInitialize(type);

            typename = type.Name;

            // HACK: bindingflags.flattenhierarchy does not return what I would expect. Create better solution
            List<FieldInfo> fields = new List<FieldInfo>();
            List<PropertyInfo> properties = new List<PropertyInfo>();

            Type t = type;
            do
            {
               t.GetFields().ToList().ForEach(fld => fields.Add(fld));

               t.GetProperties().ToList().ForEach(prp => properties.Add(prp));

                t = t.BaseType;    

            } while (t!= null && t != typeof(Object));

            // we will cache a translationmap so we can translate from propertynames to fieldnames and back
            foreach (PropertyInfo prop in properties)
            {
                string PropertyName = prop.Name;
                string fieldName = null;

                FieldInfo field = fields.SingleOrDefault(fld => fld.Name.EndsWith(PropertyName, StringComparison.OrdinalIgnoreCase));

                if (field != null)
                    fieldName = field.Name;

                if (String.IsNullOrEmpty(fieldName))
                {
                    field = fields.SingleOrDefault(fld => (fld.Name.IndexOf("<" + PropertyName + ">") == 0));

                    if (field != null)
                        fieldName = field.Name;


                    if (String.IsNullOrEmpty(fieldName))
                        throw new NotSupportedException(
                            String.Format("no translation possible for Propertyname {0} to a field, in type {1}",
                            PropertyName, type.FullName));
                }

                // will create two seperate dictionaries, because memory is cheap and 
                // performance is better this way
                PropertyNameFieldName.Add(PropertyName, fieldName);
                FieldNamePropertyName.Add(fieldName, PropertyName);
            }

        }

        public override object CreateImplementationObject(InstanceBoundLaosEventArgs eventArgs)
        {
            return new EditableBusinessObjectImplementation(eventArgs.Instance);
        }

        public override Type GetPublicInterface(Type containerType)
        {
            return typeof(IEditableBusinessObject);
        }

        public override CompositionAspectOptions GetOptions()
        {
            return CompositionAspectOptions.GenerateImplementationAccessor | CompositionAspectOptions.IgnoreIfAlreadyImplemented;
        }

        internal string FindFieldForProperty(string PropertyName)
        {
            return PropertyNameFieldName[PropertyName];
        }

        internal string FindPropertyForField(string FieldName)
        {
            return FieldNamePropertyName[FieldName];
        }
    }

    #endregion

    #region implementation of IEditableBusinessObject
    /// <summary>
    /// implementation of the interface. All calls will be delegated to this object
    /// </summary>
    [Serializable]
    public class EditableBusinessObjectImplementation : IEditableBusinessObject
    {
        #region fields and properties
        /// <summary>
        /// the instace we are implementing for
        /// </summary>
        object instance;

        /// <summary>
        /// determines if we are currently in a transaction (so, we called .beginedit())
        /// </summary>
        bool hasPendingTransaction = false;

        /// <summary>
        /// keeps values of our fields that we call the original state
        /// </summary>
        Dictionary<string, object> OriginalValues = new Dictionary<string, object>();
        /// <summary>
        /// keeps the current values (normal modus of operandus)
        /// </summary>
        Dictionary<string, object> CurrentValue = new Dictionary<string, object>();
        /// <summary>
        /// keeps values of our fields when we have a pending transaction
        /// </summary>
        Dictionary<string, object> BackupValue = new Dictionary<string, object>();
        #endregion

        #region ctor
        /// <summary>
        /// Initializes a new instance of the <see cref="EditableBusinessObjectImplementation"/> class.
        /// </summary>
        /// <param name="instance">The instance.</param>
        public EditableBusinessObjectImplementation(object instance)
        {
            this.instance = instance;
        } 
        #endregion

        #region IEditableObject Members

        /// <summary>
        /// Begins an edit on an object.
        /// This is still lightweight: from now on though, on both
        /// gets as sets, copies are created
        /// </summary>
        public void BeginEdit()
        {
            hasPendingTransaction = true;

            // need to make copy of iLists so that lists and stuff are preserved
            // however, for performance reasons, this only happens
            // when the field is actually retrieved or stored. Only then is it necessary to incur 
            // a performance hit
        }

        /// <summary>
        /// Discards changes since the last <see cref="M:System.ComponentModel.IEditableObject.BeginEdit"/> call.
        /// We only incur performance loss here. That is an optimization, because in normal 
        /// functioning, you would not expect to call canceledit much
        /// </summary>
        public void CancelEdit()
        {
            if (hasPendingTransaction)
            {
                hasPendingTransaction = false;
                foreach (KeyValuePair<string, object> members in BackupValue)
                {
                    // TODO: generate method that will set these without using reflection

                    // these are the changed fields
                    // [go through properties, so we get notifypropertychanged events]
                    instance.GetType().GetProperty(members.Key, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
                        .SetValue( instance, this.CreateCurrentObject(members.Key, members.Value), new object[] {} );
                }

                BackupValue.Clear();
            }

        }

        /// <summary>
        /// Discard our backup, so that the current version is correct
        /// </summary>
        public void EndEdit()
        {
            if (hasPendingTransaction)
            {
                BackupValue.Clear();
                hasPendingTransaction = false;
            }
        }

        #endregion

        #region IEditableBusinessObject Members
        /// <summary>
        /// Stores the value in the appropriate dictionary
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="newValue">The new value.</param>
        /// <param name="storedValue">The stored value.</param>
        /// <returns></returns>
        public object StoreValue(string propertyName, object newValue, object storedValue)
        {
            // we always store to the normal currentvalue dictionary

            // create backup of the original value if we are in pending state
            if (hasPendingTransaction)
            {
                // only need to backup the first change to the field
                if (!BackupValue.ContainsKey(propertyName))
                {
                    // we have not yet backed it up, so backup the storedvalue
                    // it should be the same as the currentvalue
                    BackupValue.Add(propertyName, CreateBackupObject(propertyName, storedValue));
                }
            }


            // and register this new value as the current value  (this is stored in the field as well!)
            if (CurrentValue.ContainsKey(propertyName))
                CurrentValue[propertyName] = newValue;
            else
                CurrentValue.Add(propertyName, newValue);

            return newValue;
        }

        /// <summary>
        /// Gets the value. This will actually just return the storedvalue, but
        /// gives the framework the opportunity to do a deepcopy, which might be
        /// needed. This way, we only do a copy if it is needed!
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="storedvalue">The storedvalue.</param>
        /// <returns>the storedvalue</returns>
        public object GetValue(string propertyName, object storedvalue)
        {
            if (hasPendingTransaction)
            {
                // only need to backup the first change to the field
                if (!BackupValue.ContainsKey(propertyName))
                {
                    // we have not yet backed it up, so backup the storedvalue
                    // it should be the same as the currentvalue
                    BackupValue.Add(propertyName, CreateBackupObject(propertyName, storedvalue));
                }
            }

            // just to be on the safe side (could have inititalized field differently)
            if (!CurrentValue.ContainsKey(propertyName))
                CurrentValue.Add(propertyName, storedvalue);

            return storedvalue;

        }


        /// <summary>
        /// Sets the value as original. Useful during deserialization, not normally used
        /// by application code
        /// </summary>
        /// <param name="PropertyName">Name of the property.</param>
        /// <param name="value">The value.</param>
        public void SetValueAsOriginal(string PropertyName, object value)
        {
            object backup = CreateBackupObject(PropertyName, value);

            if (OriginalValues.ContainsKey(PropertyName))
            {
                OriginalValues[PropertyName] = backup;
            }
            else
            {
                OriginalValues.Add(PropertyName, backup);
            }
        }

        /// <summary>
        /// Gets the original value as available in the original value dictionary.
        /// Useful during serialization, nor normally used by application code
        /// </summary>
        /// <param name="Propertyname">The propertyname.</param>
        /// <returns></returns>
        public object GetOriginalValue(string Propertyname)
        {
            return OriginalValues[Propertyname];
        }

        /// <summary>
        /// Begins the deserialization. We can do setup here.
        /// there is no setup in this implementation
        /// </summary>
        public void BeginDeserialization()
        {
        }

        /// <summary>
        /// Ends the deserialization. We can do tear down here.
        /// There is no setup in this implementation
        /// </summary>
        public void EndDeserialization()
        {
        }


        /// <summary>
        /// determines if we want to serialize the original value.
        /// Only wanted if we actually set it
        /// </summary>
        /// <param name="Propertyname">The propertyname.</param>
        /// <returns></returns>
        public bool ShouldSerializeOriginalValue(string Propertyname)
        {
            return OriginalValues.ContainsKey(Propertyname);
        }
        #endregion

        #region helper methods
        /// <summary>
        /// Creates the backup object. For normal references, this will be the original object
        /// For iLists, this will be an array with references to the original items
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="storedValue">The stored value.</param>
        /// <returns></returns>
        private object CreateBackupObject(string propertyName, object storedValue)
        {
            if (storedValue is Array && storedValue.GetType().GetGenericArguments().Length == 0)
            {
                // from the wire we get simple arrays
                Array storedArray = (Array)storedValue;
                return storedArray.Clone();
            }
            else if (storedValue is IList && storedValue.GetType().GetGenericArguments().Length == 1)
            {
                // on the client, we should be using iList implementations 
                // for our ienumerables. We can be smart about this
                IList storedList = (IList)storedValue;
                
                // the backup can be a simple array
                // however, the loaded values should not be arrays, but lists. That's better during serialization

                // we will copy the items back into the list when needed
                IList newList = (IList) Activator.CreateInstance(storedList.GetType());

                foreach (object item in storedList)
                    newList.Add(item);


                return newList;

            }
            else if (storedValue is ICollection)
            {
                throw new NotSupportedException("Can not create backup of ienumerables that do not implement iList");
            }
            else
            {
                // we can just return the reference to our object.
                return storedValue;
            }
        }

        /// <summary>
        /// Creates the current object. For normal references, nothing is done
        /// for Ilists, this will restore the corresponding value from the currentValues list
        /// to the original items
        /// </summary>
        /// <param name="propertyName">Name of the property.</param>
        /// <param name="backedupValue">The backedup value.</param>
        /// <returns></returns>
        private object CreateCurrentObject(string propertyName, object backedupValue)
        {
            if (backedupValue.GetType().IsArray)
            {
                // we need to take the current object, clear it, and fill with the objects here
                object[] backedupArray = (object[])backedupValue;

                IList lst = (IList)this.CurrentValue[propertyName];
                lst.Clear();

                foreach (object orig in backedupArray)
                    lst.Add(orig);

                return lst;

            }
            else
            {
                return backedupValue;
            }
        }
        #endregion
    }

    #endregion
}
